﻿// -----------------------------------------------------------------------
// <copyright file="GraphicEqualizer.cs" company="Abyssalsoft">
// TODO: Update copyright text.
// </copyright>
// -----------------------------------------------------------------------

namespace Core.Player.Audio.Filters
{
    using System;
    using System.Collections.Generic;
    using System.Drawing;
    using System.Runtime.InteropServices;
    using System.Text;
    using System.Windows.Forms;
    using Un4seen.Bass;
    using Un4seen.Bass.Misc;

    /// <summary>
    /// TODO: Update summary.
    /// </summary>
    public class GraphicEqualizer
    {
        #region Private members

        /// <summary>Graphic equalizer style</summary>
        private static int visualisationStyle = 3;

        /// <summary>Delegate DSP Process</summary>
        private DSPPROC myDSPAddr = null;

        /// <summary>Limit to x milliseconds</summary>
        private int mslength = 0;

        /// <summary>Stores device latency in milliseconds</summary>
        private int deviceLatencyMS = 0;

        /// <summary>Stores device latency in bytes</summary>
        private int deviceLatencyBytes = 0;

        /// <summary>Visuals class instance</summary>
        private Visuals vis = new Visuals();

        /// <summary>The interval to update graphic equalizer (Recommended: 50 milliseconds)</summary>
        private int updateInterval = 50;

        /// <summary>Declare BASS timer</summary>
        private Un4seen.Bass.BASSTimer updateTimer = null;

        /// <summary>
        /// This is the local buffer, we use it outside of MyDSPGain for better performance
        /// and to reduce the need to allocate it every time MyDSPGain is called!
        /// </summary>
        private float[] data;

        /// <summary>This local member keeps the amplification level as a float value</summary>
        private float gainAmplification = 1;

        /// <summary>This is our picture box object, where we draw the graphic equalizer</summary>
        private PictureBox graphicImage;

        #endregion

        #region Styles attributes

        /// <summary>Graphic equalizer, gradient color 1</summary>
        private Color color1;

        /// <summary>Graphic equalizer, gradient color 2</summary>
        private Color color2;

        /// <summary>The back color of the graphic equalizer</summary>
        private Color backColor;

        /// <summary>Show a linear graphic equalizer</summary>
        private bool linear;

        /// <summary>Show a full spectrum graphic equalizer</summary>
        private bool fullSpectrum;

        /// <summary>Show a high quality graphic equalizer (uses more CPU)</summary>
        private bool highQuality;

        /// <summary>Graphic equalizer bar color</summary>
        private Color color3;

        /// <summary>The line width of the wave (only on default spectrum style)</summary>
        private int lineWidth;

        /// <summary>The dot width of the wave (only on dot style)</summary>
        private int dotWidth;

        /// <summary>The bean width of the wave (only on bean style)</summary>
        private int beanWidth;

        /// <summary>The ellipse width of the wave (only on ellipse style)</summary>
        private int ellipseWidth;

        /// <summary>The wave width of the wave (only on wave style)</summary>
        private int waveWidth;

        /// <summary>The distance between the graphic bars</summary>
        private int distance;

        /// <summary>The width of the peak</summary>
        private int peakWidth;

        /// <summary>The peak delay - how much time to remain up, before will fall down</summary>
        private int peakDelay;

        #endregion

        #region Constructor

        /// <summary> Initializes a new instance of the GraphicEqualizer class. </summary>
        /// <param name="outputImage">The picture box where to draw the graphic equalizer</param>
        internal GraphicEqualizer(ref PictureBox outputImage)
        {
            // Activate our licence
            Un4seen.Bass.BassNet.Registration("marius@abyssalsoft.com", "2X223282231122");

            this.graphicImage = outputImage;
            
            Events.Instance.Play += new EventHandler(this.OnPlay);
            Events.Instance.Stop += new EventHandler(this.OnStop);
        }

        #endregion

        #region Properties

        /// <summary>Gets or sets gradient color 1</summary>
        public Color Color1
        {
            get
            {
                return this.color1;
            }

            set
            {
                this.color1 = value;
            }
        }

        /// <summary>Gets or sets gradient color 2</summary>
        public Color Color2
        {
            get
            {
                return this.color2;
            }

            set
            {
                this.color2 = value;
            }
        }

        /// <summary>Gets or sets the back color of the graphic equalizer</summary>
        public Color BackColor
        {
            get
            {
                return this.backColor;
            }

            set
            {
                this.backColor = value;
            }
        }

        /// <summary>Gets or sets a value indicating whether to display a linear graphic equalizer</summary>
        public bool Linear
        {
            get
            {
                return this.linear;
            }

            set
            {
                this.linear = value;
            }
        }

        /// <summary>Gets or sets a value indicating whether to display a full spectrum graphic equalizer</summary>
        public bool FullSpectrum
        {
            get
            {
                return this.fullSpectrum;
            }

            set
            {
                this.fullSpectrum = value;
            }
        }

        /// <summary>Gets or sets a value indicating whether to show a high quality graphic equalizer (uses more CPU)</summary>
        public bool HighQuality
        {
            get
            {
                return this.highQuality;
            }

            set
            {
                this.highQuality = value;
            }
        }

        /// <summary>Gets or sets the graphic equalizer bar color</summary>
        public Color Color3
        {
            get
            {
                return this.color3;
            }

            set
            {
                this.color3 = value;
            }
        }

        /// <summary>Gets or sets the line width of the wave (only on default spectrum style)</summary>
        public int LineWidth
        {
            get
            {
                return this.lineWidth;
            }

            set
            {
                this.lineWidth = value;
            }
        }

        /// <summary>Gets or sets the dot width of the wave (only on dot style)</summary>
        public int DotWidth
        {
            get
            {
                return this.dotWidth;
            }

            set
            {
                this.dotWidth = value;
            }
        }

        /// <summary>Gets or sets the bean width of the wave (only on bean style)</summary>
        public int BeanWidth
        {
            get
            {
                return this.beanWidth;
            }

            set
            {
                this.beanWidth = value;
            }
        }

        /// <summary>Gets or sets the ellipse width of the wave (only on ellipse style)</summary>
        public int EllipseWidth
        {
            get
            {
                return this.ellipseWidth;
            }

            set
            {
                this.ellipseWidth = value;
            }
        }

        /// <summary>Gets or sets the wave width of the wave (only on wave style)</summary>
        public int WaveWidth
        {
            get
            {
                return this.waveWidth;
            }

            set
            {
                this.waveWidth = value;
            }
        }

        /// <summary>Gets or sets the distance between the graphic bars</summary>
        public int Distance
        {
            get
            {
                return this.distance;
            }

            set
            {
                this.distance = value;
            }
        }

        /// <summary>Gets or sets the width of the peak</summary>
        public int PeakWidth
        {
            get
            {
                return this.peakWidth;
            }

            set
            {
                this.peakWidth = value;
            }
        }

        /// <summary>Gets or sets the peak delay - how much time to remain up, before will fall down</summary>
        public int PeakDelay
        {
            get
            {
                return this.peakDelay;
            }

            set
            {
                this.peakDelay = value;
            }
        }

        /// <summary>
        /// Gets or sets the Graphic equalizer style
        /// </summary>
        internal static int VisualisationStyle
        {
            get
            {
                return visualisationStyle;
            }

            set
            {
                visualisationStyle = value;
            }
        }

        #endregion

        #region Events

        /// <summary>
        /// Start graphic equalizer when a media file start playing
        /// </summary>
        /// <param name="sender">Object sender</param>
        /// <param name="e">Event args</param>
        private void OnPlay(object sender, EventArgs e)
        {
            if (Core.Player.MediaInfo.CurrentFile != Core.Player.Helper.FileTypeEnum.LocalAudio &&
                Core.Player.MediaInfo.CurrentFile != Core.Player.Helper.FileTypeEnum.InternetAudioStream)
            {
                if (this.updateTimer != null)
                {
                    this.updateTimer.Stop();
                }

                return;
            }

            // Create a secure timer
            this.updateTimer = new Un4seen.Bass.BASSTimer(this.updateInterval);
            this.updateTimer.Tick += new EventHandler(this.TimerUpdate_Tick);

            // Used in RMS (30ms window)
            this.mslength = (int)Bass.BASS_ChannelSeconds2Bytes(AudioCommon.Instance.Stream, 0.03);

            // Latency from milliseconds to bytes
            this.deviceLatencyBytes = (int)Bass.BASS_ChannelSeconds2Bytes(AudioCommon.Instance.Stream, this.deviceLatencyMS / 1000.0);

            // Set a DSP user callback method
            this.myDSPAddr = new DSPPROC(this.MyDSPGainUnsafe);
            Bass.BASS_ChannelSetDSP(AudioCommon.Instance.Stream, this.myDSPAddr, IntPtr.Zero, 2);

            this.updateTimer.Start();
        }

        /// <summary>
        /// Stop graphic equalizer when the playback is stopped
        /// </summary>
        /// <param name="sender">Object sender</param>
        /// <param name="e">Event args</param>
        private void OnStop(object sender, EventArgs e)
        {
            if (this.updateTimer != null)
            {
                this.updateTimer.Stop();
            }

            if (this.graphicImage != null)
            {
                this.graphicImage.Image = null;
            }
        }

        #endregion

        #region Methods

        /// <summary>Timer tick event</summary>
        /// <param name="sender">Object sender</param>
        /// <param name="e">Event arguments</param>
        private void TimerUpdate_Tick(object sender, System.EventArgs e)
        {
            try
            {
                // here we gather info about the stream, when it is playing...
                if (Bass.BASS_ChannelIsActive(AudioCommon.Instance.Stream) == BASSActive.BASS_ACTIVE_PLAYING)
                {
                    // the stream is still playing...
                }
                else
                {
                    // the stream is NOT playing anymore...
                    this.graphicImage.Image = null;
                    return;
                }

                //// from here on, the stream is for sure playing...
                this.DrawSpectrum();
            }
            catch
            {
                // Throw...
            }
        }

        /// <summary>DSP callback - safe</summary>
        /// <param name="handle">Current playback handle</param>
        /// <param name="channel">Channel as integer</param>
        /// <param name="buffer">Buffer pointer</param>
        /// <param name="length">Buffer length</param>
        /// <param name="user">Current user</param>
        private void MyDSPGain(int handle, int channel, System.IntPtr buffer, int length, int user)
        {
            try
            {
                if (this.gainAmplification == 1f || length == 0 || buffer == IntPtr.Zero)
                {
                    return;
                }

                // length is in bytes, so the number of floats to process is:
                // length / 4 : byte = 8-bit, float = 32-bit
                int l4 = length / 4;

                // increase the data buffer as needed
                if (this.data == null || this.data.Length < l4)
                {
                    this.data = new float[l4];
                }

                // copy from unmanaged to managed memory
                Marshal.Copy(buffer, this.data, 0, l4);

                // apply the gain, assumeing using 32-bit floats (no clipping here ;-)
                for (int a = 0; a < l4; a++)
                {
                    this.data[a] = this.data[a] * this.gainAmplification;
                }

                // copy back from managed to unmanaged memory
                Marshal.Copy(this.data, 0, buffer, l4);
            }
            catch
            {
            }
        }

        /// <summary>DSP callback - unsafe
        /// This allows us to use pointers pretty much like in C/C++
        /// This is fast, efficient, but is NOT safe (e.g. no overflow handling, no type checking etc.)
        /// But also there is no need to Marshal and Copy any data between managed and unmanaged code
        /// So... use with careful!
        /// </summary>
        /// <param name="handle">Current playback handle</param>
        /// <param name="channel">Channel as integer</param>
        /// <param name="buffer">Buffer pointer</param>
        /// <param name="length">Buffer length</param>
        /// <param name="user">Current user</param>
        private unsafe void MyDSPGainUnsafe(int handle, int channel, IntPtr buffer, int length, IntPtr user)
        {
            try
            {
                if (this.gainAmplification == 1f || length == 0 || buffer == IntPtr.Zero)
                {
                    return;
                }

                // length is in bytes, so the number of floats to process is: length / 4 : byte = 8-bit, float = 32-bit
                int l4 = length / 4;

                float* data = (float*)buffer;
                for (int a = 0; a < l4; a++)
                {
                    data[a] = data[a] * this.gainAmplification;
                    //// alternatively we can also use:
                    //// *data = *data * _gainAmplification;
                    //// data++;  // moves the pointer to the next float
                }
            }
            catch
            {
            }
        }

        /// <summary>Draws the spectrum in our picture box object</summary>
        private void DrawSpectrum()
        {
            switch (visualisationStyle)
            {
                // Spectrum
                case 0:
                    this.graphicImage.Image = this.vis.CreateSpectrum(
                        AudioCommon.Instance.Stream, this.graphicImage.Width, this.graphicImage.Height, this.Color1, this.Color2, this.BackColor, this.Linear, this.FullSpectrum, this.HighQuality);
                    break;

                // Spectrum line
                case 1:
                    this.graphicImage.Image = this.vis.CreateSpectrumLine(
                        AudioCommon.Instance.Stream, this.graphicImage.Width, this.graphicImage.Height, this.Color1, this.Color2, this.BackColor, this.LineWidth, this.Distance, this.Linear, this.FullSpectrum, this.HighQuality);
                    break;

                // Spectrum ellipse
                case 2:
                    this.graphicImage.Image = this.vis.CreateSpectrumEllipse(
                        AudioCommon.Instance.Stream, this.graphicImage.Width, this.graphicImage.Height, this.Color1, this.Color2, this.BackColor, this.EllipseWidth, this.Distance, this.Linear, this.FullSpectrum, this.HighQuality);
                    break;

                // Spectrum dot
                case 3:
                    this.graphicImage.Image = this.vis.CreateSpectrumDot(
                        AudioCommon.Instance.Stream, this.graphicImage.Width, this.graphicImage.Height, this.Color1, this.Color2, this.BackColor, this.DotWidth, this.Distance, this.Linear, this.FullSpectrum, this.HighQuality);
                    break;

                // Spectrum line peak (default)
                case 4:
                    this.graphicImage.Image = this.vis.CreateSpectrumLinePeak(
                        AudioCommon.Instance.Stream, this.graphicImage.Width, this.graphicImage.Height, this.Color1, this.Color2, this.Color3, this.BackColor, this.LineWidth, this.PeakWidth, this.Distance, this.PeakDelay, this.Linear, this.FullSpectrum, this.HighQuality);
                    break;

                // Spectrum wave
                case 5:
                    this.graphicImage.Image = this.vis.CreateSpectrumWave(
                        AudioCommon.Instance.Stream, this.graphicImage.Width, this.graphicImage.Height, this.Color1, this.Color2, this.BackColor, this.WaveWidth, this.Linear, this.FullSpectrum, this.HighQuality);
                    break;

                // Spectrum bean
                case 6:
                    this.graphicImage.Image = this.vis.CreateSpectrumBean(
                        AudioCommon.Instance.Stream, this.graphicImage.Width, this.graphicImage.Height, this.Color1, this.Color2, this.BackColor, this.BeanWidth, this.Linear, this.FullSpectrum, this.HighQuality);
                    break;

                // Spectrum text
                case 7:
                    this.graphicImage.Image = this.vis.CreateSpectrumText(
                        AudioCommon.Instance.Stream, this.graphicImage.Width, this.graphicImage.Height, this.Color1, this.Color2, this.BackColor, MediaInfo.FileName, this.Linear, this.FullSpectrum, this.HighQuality);
                    break;

                // Spectrum Frequency detection
                case 8:
                    float amp = this.vis.DetectFrequency(AudioCommon.Instance.Stream, 10, 500, true);
                    if (amp > 0.3)
                    {
                        this.graphicImage.BackColor = this.Color1;
                    }
                    else
                    {
                        this.graphicImage.BackColor = this.Color2;
                    }

                    break;
            }
        }

        #endregion
    }
}
